Conversation
…+ executor.Stat PR-26-code-C is split into two reviewable sub-slices on the same branch. C1 (this commit) lands the WRITER side; C2 (next commit) lands the READER side. §50 ordering lock: writer commit BEFORE reader. Authority: - PR #512 / contract.md Part IV §§37-50 - PR #513 / §51 lock record (§51.5-A2: read-only typed introspection is OUTSIDE the bounded-3 mutation cap) - PR #514 / code-A merge 4e98ff5 - PR #515 / code-B merge 45fc63e - §42 cron backup / A.4 contract - §51.6 entry criteria (code-B merged) C1 scope (this commit): 1. Add typed executor.Stat read-only introspection method. - executor/executor.go: new FileMeta struct + Stat method on Executor interface. - executor/real.go: RealExecutor.Stat via os.Stat + syscall.Stat_t. UID/GID extracted from the platform-specific Sys() interface (Linux-only target). - executor/mock.go: MockExecutor.Stat reads from new FileStats map (path → FileMeta); falls back to (0644, 0:0, len(content)) if the path is in Files but not FileStats. Returns os.ErrNotExist if neither holds. - Per §51.5-A2 invariant: read-only introspection is OUTSIDE the bounded-3 mutation surface cap of INV-PR26-NEW-MUTATION-SURFACES-BOUNDED. Stat does NOT count against §44 row 2's mutation budget. 2. New shared cron-manifest module: internal/installer/switchop/cron_manifest.go. - Constants: CronManifestSchemaVersion = "1.0.0" CronManifestDir = "/var/lib/nftban/state/csf-cron-backup" CronManifestFile = "/var/lib/nftban/state/csf-cron-backup/manifest.json" CronCSFSrcPath = "/etc/cron.d/csf-cron" CronLFDSrcPath = "/etc/cron.d/lfd-cron" - Types: CronManifestEntry (path / backup_name / sha256 / mode / uid / gid / size) + CronManifest (schema_version / captured_at / files). - Helpers: ComputeCronBackupSHA256(content) — single source of truth shared by writer + reader; identical bytes-to-hex semantics in both directions. WriteCronBackupManifest(exec, log) — install-time writer. For each of {csf-cron, lfd-cron} that exists: read content, Stat for mode/uid/gid/size, compute sha256, copy under CronManifestDir, append manifest entry. Then write manifest.json. Files absent at capture time are skipped (no entry recorded; no fabrication). ReadCronBackupManifest(exec, log) — used by the C2 reader. Three return shapes: absent (zero, false, nil), present-but- corrupt (zero, true, ErrCronManifestParseFailed/ ErrCronManifestSchemaMismatch/ErrCronManifestUnknownEntry), present-and-valid (manifest, true, nil). VerifyCronBackupEntry(exec, entry) — sha256 integrity check against the on-disk backup. - Sentinels: ErrCronManifestSchemaMismatch, ErrCronManifestSHA256Mismatch, ErrCronManifestUnknownEntry, ErrCronManifestParseFailed. 3. Modified disarmCSFArtifacts in switchop/takeover.go to call WriteCronBackupManifest BEFORE the existing rm -f of the cron files. Writer failure is logged but non-fatal: the rm path MUST still execute (nftban-takeover correctness invariant). Hosts installed before PR-26-code-C ship without a manifest; A.4 stays soft-skip on those hosts (§42.2 graceful migration). 4. Tests in internal/installer/switchop/cron_manifest_test.go: - WriteCronBackupManifest_BothPresent_RecordsBoth - WriteCronBackupManifest_OnlyOnePresent_OnlyOneRecorded - WriteCronBackupManifest_NeitherPresent_EmptyManifest - WriteCronBackupManifest_WritesOnlyManifestDir (no writes outside CronManifestDir) - WriteCronBackupManifest_ManifestPathPinnedExact - WriteCronBackupManifest_OnlyAuthorizedSrcPaths (writer ignores non-{csf-cron, lfd-cron} cron files; never invents content) - WriteCronBackupManifest_SHA256ComputedCorrectly - ReadCronBackupManifest_AbsentReturnsFalse (graceful skip path) - ReadCronBackupManifest_ParseFailure (corrupt JSON refused) - ReadCronBackupManifest_SchemaMismatch - ReadCronBackupManifest_UnknownEntryPath (defense-in-depth) - ReadCronBackupManifest_HappyPath - CronManifest_WriteThenRead_Roundtrip - VerifyCronBackupEntry_HappyPath - VerifyCronBackupEntry_SHA256Mismatch Constraints honored (per §51.6 + operator C scope): IN scope (C1): - install-time cron-backup manifest writer ✓ - only the two §42.2-locked cron files (csf-cron, lfd-cron) ✓ - only writes under CronManifestDir ✓ - manifest records: path, sha256, mode, uid, gid, size, schema_version ✓ - no template regeneration ✓ - no DirectAdmin custombuild ✓ - no unrelated cron files ✓ - absent files cleanly skipped (no fabrication) ✓ OUT of scope (and untouched): - A.4 reader / restore path (PR-26-code-C2 in next commit on same branch) - Destructive real-host CSF soak (PR-26-code-E) - IptablesRuleExists / iptables introspection (Option B lock) - main.go / state-machine / exit codes / history gate (untouched) - Restore planner / TargetAuthority / PR-24 lattice (untouched) - contract.md (untouched) - Repo hygiene / UX / GOTH / metrics / module cleanup (untouched) Verified on lab2 (Ubuntu 24.04, go1.22.2): - go build ./... clean - go test ./internal/installer/switchop/... PASS C2 lands the reader side in the next commit on this branch. Both ship in PR-26-code-C; auditor checkpoint after C1+C2 compile + tests pass before push. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… step 3
PR-26-code-C2 — companion to C1. C1 lands the install-time manifest
writer; C2 (this commit) flips A.4 from soft-skip to manifest-restore
when the §42.2 cron-backup manifest is present + integrity-clean.
Authority:
- C1 commit on this branch (cron_manifest.go writer + executor.Stat)
- §42.2 cron-backup contract (manifest-only restore; no template
regeneration; no cron files NFTBan did not back up itself)
- §51.6 entry criteria
Behavior delta:
- Before (C1): A.4 always soft-skipped with a generic warning.
- After (C2): A.4 reads switchop.ReadCronBackupManifest. Three paths:
- Manifest absent (pre-PR-26 host) → graceful soft-skip, no
/etc/cron.d/* writes, A.5 runs.
- Manifest present but corrupt / schema-mismatch / unknown-entry /
sha256-mismatch → soft-skip with a specific operator warning,
no /etc/cron.d/* writes, A.5 still runs (per §42.2-D: csf can
function without cron; LFD just won't auto-restart).
- Manifest present + integrity-clean → for each entry whose target
is currently absent, restore via WriteFileAtomic (preserves
mode) + Chown (preserves uid/gid). Targets that already exist
are skipped (operator may have re-created a different version
post-takeover; A.4 must not overwrite operator content).
Files changed (2):
cmd/nftban-installer/restore_deps_csf.go
- New typed sentinel: ErrCSFRestoreCronManifestCorrupt (exported for
observability + test assertion via errors.Is). Per §42.2-D, A.4
emits this informationally and continues to A.5; the overall
mutation does NOT abort on cron failure.
- A.4 step rewritten: calls switchop.ReadCronBackupManifest,
switches on (absent / corrupt / present), per-entry sha256
verification via switchop.VerifyCronBackupEntry, restoration via
exec.WriteFileAtomic + exec.Chown.
- New imports: "os" (for os.FileMode), "switchop" (for the shared
manifest module).
- New local helper fileModeFromUint32 — single-purpose conversion
for the manifest's uint32 mode bitfield to os.FileMode. Keeps os
import scoped narrowly.
cmd/nftban-installer/restore_deps_csf_test.go
- New seedCronManifest helper writes a sha256-valid manifest +
matching backup files into the mock for end-to-end A.4 tests.
- 8 new TestCSFMutate_PR26C2_* tests:
1. A4_ManifestAbsent_SoftSkip — pre-PR-26 host case
2. A4_HappyPath_RestoresBothFiles — manifest present + integrity
clean + targets absent
3. A4_TargetExists_SkipsRestore — operator content not overwritten
4. A4_SHA256Mismatch_SoftSkip_A5StillRuns — §42.2-D non-abort
5. A4_SchemaMismatch_SoftSkip_A5StillRuns — §42.2-D non-abort
6. A4_OnlyAuthorizedTargetPaths — no broad /etc/cron.d/* writes
7. TypedSentinelExported — ErrCSFRestoreCronManifestCorrupt visible
8. A4_UnknownEntryPath_Rejected — defense-in-depth refusal
Constraints honored (per §51.6 + operator C scope):
IN scope (C2):
- A.4 reader / restore path enabled when manifest is present ✓
- soft-skip with warning for pre-PR-26 hosts ✓
- typed refusal (sentinel surfaced) for corrupt / hash-mismatch /
ambiguous cases ✓
- restore only the two §42.2-locked cron files ✓
- preserve mode/uid/gid via WriteFileAtomic + Chown ✓
- no write outside the two backup-target paths ✓
- no cron restore unless evidence says NFTBan backed up the file ✓
OUT of scope (and untouched):
- Destructive real-host CSF soak (PR-26-code-E)
- IptablesRuleExists / iptables introspection (Option B lock)
- main.go / state-machine / exit codes / history gate
- Restore planner / TargetAuthority / PR-24 lattice
- contract.md
- Repo hygiene / UX / GOTH / metrics / module cleanup
§42.2-D semantics preserved: A.4 corrupt-manifest does NOT abort A.5.
csf can function without cron; LFD just won't auto-restart. The
operator-warning log line is more specific than 4B-3-csf's generic
warning (states which precondition failed). The typed sentinel is
exposed for higher-layer observability.
Verified on lab2 (Ubuntu 24.04, go1.22.2):
- go build ./... clean
- go test ./cmd/nftban-installer/... ./internal/installer/restore/...
./internal/installer/state/... ./internal/installer/executor/...
./internal/installer/switchop/... PASS
- 8 new TestCSFMutate_PR26C2_* tests all PASS
- existing TestCSFMutate_4B3csf_A4_SoftSkip_ZeroFileWrites still
passes (manifest-absent fixture takes the new soft-skip path)
Awaiting C1+C2 auditor checkpoint before push. CI gate update
(G4-RESTORE-CRON-MANIFEST-INTEGRITY) lands as a third commit on
the same branch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tural gate
Strengthens the Restore Canonization workflow with the §46 cron-
manifest integrity gate locked at §51.6 entry criteria for code-C.
Authority:
- §42 cron backup / A.4 contract (manifest-only restore)
- §46 CI gate requirements (structural, not loose grep)
- §46.1 line-skipping discipline (production-code-only,
comment-stripped)
Gate scope (writer + reader cross-pin):
WRITER required symbols (internal/installer/switchop/cron_manifest.go):
- CronManifestSchemaVersion = "1.0.0" const
- CronManifestDir / CronManifestFile constants pinned to the exact
/var/lib/nftban/state/csf-cron-backup/{,manifest.json} paths
- CronCSFSrcPath / CronLFDSrcPath constants pinned to the exact
/etc/cron.d/{csf-cron,lfd-cron} source paths
- func ComputeCronBackupSHA256(content []byte) string — single
source of truth for the sha256 helper
- func WriteCronBackupManifest(...), ReadCronBackupManifest(...),
VerifyCronBackupEntry(...) — the three exported API points
- sha256.Sum256 — proves the writer actually computes sha256 (not a
no-op stub)
Pattern shape: whitespace-flexible ([[:space:]]+) so the patterns
don't break when gofmt re-aligns the const block.
READER required symbols (cmd/nftban-installer/restore_deps_csf.go):
- switchop.ReadCronBackupManifest( — A.4 reads the manifest
- switchop.VerifyCronBackupEntry( — A.4 verifies sha256 BEFORE
restoring (this is the integrity guarantee §42.2-D requires)
- ErrCSFRestoreCronManifestCorrupt — the typed sentinel surfaced
on integrity failure
If any required symbol is absent, the gate fails — proves the
integrity check is consumed, not just imported.
WRITER + READER forbidden patterns:
- \bcustombuild\b — defense-in-depth (§34: no DirectAdmin custombuild)
- iptables-restore — defense-in-depth (§34: csf manages its own)
- "/etc/cron.d/*" glob literal — no broad cron sweep
- WriteFile to /etc/cron.d/* with non-csf-prefixed leaf (rough check)
READER allow-list pin:
- Every WriteFileAtomic call in restore_deps_csf.go that targets a
/etc/cron.d/* literal MUST equal one of the two §42.2-locked
literals: "/etc/cron.d/csf-cron" OR "/etc/cron.d/lfd-cron".
- The reader uses the named constants csfCronPath / lfdCronPath, so
in practice this grep returns zero matches (named-constant
reference, not string-literal in WriteFileAtomic args). Defense-
in-depth structural pin against accidental future literal-arg
drift.
§46.1 discipline applied: production-code-only files, comment-
stripped before pattern matching. Avoids the false-positive class
that hit Policy Gates on PR #511 (//-comment text matching forbidden
substrings).
Local replay against the PR-26-code-C1 + C2 source:
WRITER_MISS / READER_MISS / FORBIDDEN_HIT / BAD_LITERAL: all 0
FAIL=0
Verified on lab2 (Ubuntu 24.04, go1.22.2):
- go build ./... clean
- go test ./... PASS (64 packages)
- go test -race -count=1 ./cmd/nftban-installer
./internal/installer/restore/... ./internal/installer/state/...
./internal/installer/switchop/... PASS
- go vet ./... clean
- go mod tidy no-op
Auditor checkpoint: C1 + C2 + CI gate are now all locally compiled,
tested, and gate-replayed clean. Awaiting focused auditor pass before
push.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…soft-skip (auditor verdict)
Auditor focused-audit on PR-26-code-C flagged a semantic risk in
the A.4 corrupt-manifest branch: previously a corrupt /
hash-mismatch / unknown-entry / parse-failure manifest was a
soft-skip with an informational sentinel, and A.5 still ran. The
auditor argued — correctly — that proceeding to start csf.service
when restore evidence is on disk but cannot be trusted weakens the
evidence chain.
Locked rule (per auditor verdict):
manifest absent → soft-skip warning, continue to A.5 [migration gap, kept]
manifest incomplete → ErrCSFRestoreCronManifestCorrupt, stop before A.5
hash mismatch → ErrCSFRestoreCronManifestCorrupt, stop before A.5
target exists dirty → ErrCSFRestoreCronTargetExists, stop before A.5
manifest clean → restore exact files, then continue to A.5
Behavior delta (this commit only — C1 + C2 + CI gate semantics
remain otherwise unchanged):
- Manifest parse failure / schema mismatch / unknown-entry path
→ A.4 returns wrapped ErrCSFRestoreCronManifestCorrupt; A.5 does
NOT run; the existing §32 step-3 failure path retains the safety
net.
- Per-entry sha256 mismatch → same hard refusal.
- Operator-content collision (target /etc/cron.d/<name> already
exists) → A.4 returns wrapped ErrCSFRestoreCronTargetExists; A.5
does NOT run.
- Manifest absent (pre-PR-26 host) → unchanged: graceful soft-skip
with operator warning, control falls through to A.5.
- Manifest clean → unchanged: restore both files, fall through to
A.5.
Files changed:
cmd/nftban-installer/restore_deps_csf.go
- ErrCSFRestoreCronManifestCorrupt docstring rewritten: now
documents hard-refusal semantics (was: informational soft-skip).
Wording updated: "refusing before A.5 (operator must inspect)".
- New typed sentinel ErrCSFRestoreCronTargetExists for the
operator-content-collision case. Distinct from
ErrCSFRestoreCronManifestCorrupt for cleaner classification: a
collision is an evidence conflict, not a manifest-trust failure.
- A.4 step rewritten:
* manifestErr branch now returns the wrapped sentinel instead
of falling through.
* Per-entry sha256 verify failure now returns instead of skip.
* Per-entry unauthorized-Path now returns instead of skip.
* Per-entry target-exists collision now returns
ErrCSFRestoreCronTargetExists instead of skip.
* Per-entry WriteFileAtomic failure now returns instead of skip.
* Chown failure remains soft (logged warning, content already
restored — partial-restore is recoverable; the integrity
chain is unaffected).
cmd/nftban-installer/restore_deps_csf_test.go
- Renamed + retargeted three tests to assert hard-refusal:
PR26C2_A4_TargetExists_SkipsRestore
→ PR26C2_A4_TargetExists_HardRefuses_StopsBeforeA5
+ asserts errors.Is(err, ErrCSFRestoreCronTargetExists)
+ asserts NOT mock.CommandCalled("systemctl","start",csf.service)
PR26C2_A4_SHA256Mismatch_SoftSkip_A5StillRuns
→ PR26C2_A4_SHA256Mismatch_HardRefuses_StopsBeforeA5
+ asserts errors.Is(err, ErrCSFRestoreCronManifestCorrupt)
+ asserts A.5 NOT called
PR26C2_A4_SchemaMismatch_SoftSkip_A5StillRuns
→ PR26C2_A4_SchemaMismatch_HardRefuses_StopsBeforeA5
+ asserts errors.Is(err, ErrCSFRestoreCronManifestCorrupt)
+ asserts A.5 NOT called
PR26C2_A4_UnknownEntryPath_Rejected
→ PR26C2_A4_UnknownEntryPath_HardRefuses_StopsBeforeA5
+ asserts errors.Is(err, ErrCSFRestoreCronManifestCorrupt)
+ asserts A.5 NOT called
- 3 new tests pinning the kept-behavior branches:
PR26C2_A4_HappyPath_ContinuesToA5 — clean restore continues
PR26C2_A4_ManifestAbsent_ContinuesToA5 — migration soft-skip continues
PR26C2_A4_ParseFailure_HardRefuses_StopsBeforeA5 — parse failure stops
Push criteria (all met as of this commit):
- manifest absent = migration soft-skip ✓ (test #10 above)
- manifest corrupt/hash mismatch = typed refusal before A.5 ✓ (tests #4, #5, #8, #11)
- target cron path broad writes = impossible ✓ (allow-list + writer scope)
- writer-before-reader invariant = tested ✓ (C1's roundtrip + C2's HappyPath_RestoresBothFiles)
- G4-RESTORE-CRON-MANIFEST-INTEGRITY = PASS (local replay clean)
- go test ./... + race + vet = PASS on lab2
Verified on lab2 (Ubuntu 24.04, go1.22.2):
- go build ./... clean
- go test ./... (full repo) PASS
- go test -race -count=1 cmd + restore + state + switchop PASS
- go vet ./... clean
- 11 TestCSFMutate_PR26C2_* tests all PASS (3 hard-refusal tests
retargeted; 1 unchanged; 7 unchanged or new)
- existing PR-25 / PR-26-code-A / PR-26-code-B tests all still pass
- G4-RESTORE-CRON-MANIFEST-INTEGRITY local replay: FAIL=0
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
…STORE-EXEC-NO-OUT-OF-TARGET Classification: CI gate stale after authorized A.4 write became real, not a production-code defect. The G4-RESTORE-EXEC-NO-OUT-OF-TARGET gate was authored before A.4 became real (PR-25 commit 5 + tightened in PR-26-code-B). At that time, A.4 was a soft-skip with no legitimate file-write path, so a broad \bexec\.WriteFileAtomic\( forbid was correct. PR-26-code-C2 changed that: A.4 now legitimately writes to /etc/cron.d/csf-cron and /etc/cron.d/lfd-cron (and ONLY those two paths) when the §42.2 manifest is present and integrity-clean. The broad forbid is now stale and trips on legitimate code. Resolution per auditor verdict + operator decision: drop the \bexec\.WriteFileAtomic\( line from G4-RESTORE-EXEC-NO-OUT-OF-TARGET forbidden_patterns and rely on the dedicated G4-RESTORE-CRON-MANIFEST-INTEGRITY gate (added in commit 93e86e2) to authorize and constrain A.4 writes structurally: G4-RESTORE-EXEC-NO-OUT-OF-TARGET = forbid broad / unrelated mutation surfaces G4-RESTORE-CRON-MANIFEST-INTEGRITY = authorize and constrain the exact A.4 cron-restore writes (writer + reader symbol pin, cron-target literal allow-list, sha256-helper presence) Carving line-exceptions into the EXEC gate was rejected — that recreates the regex-brittleness class flagged at PR #515. Two gates with separate scopes is cleaner than one gate with carve-outs. Files changed: only .github/workflows/ci-restore-canonization.yml. - Removed pattern: '\bexec\.WriteFileAtomic\(' - Added explanatory comment block above the forbidden_patterns pointing at G4-RESTORE-CRON-MANIFEST-INTEGRITY for cron-write authorization. Kept (unchanged): - os.WriteFile / os.Create / os.Remove / os.Rename / exec.Command forbids - ServiceMask / ServiceDisable / DaemonReload forbids - raw mutating Run("systemctl", verb, …) forbids (9 verbs) - raw Run("mv", …) forbid - NftDeleteTable allow-list pin (ip:nftban / ip6:nftban only) - §46.1 line-skipping discipline - G4-RESTORE-CRON-MANIFEST-INTEGRITY gate (entirely) Local replay (exact CI workflow bash, against PR-26-code-C head): G4-RESTORE-EXEC-NO-OUT-OF-TARGET fail=0 G4-RESTORE-CRON-MANIFEST-INTEGRITY fail=0 No production code touched. Production semantics from C1 + C2 + the hard-refusal fix (f7be0c4) all unchanged. Pre-PR-26 hosts continue to soft-skip; A.4 hard-refuses on corrupt evidence; A.5 only runs when restore evidence is trusted. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8 tasks
itcmsgr
added a commit
that referenced
this pull request
Apr 28, 2026
…6 lock) (#517) * feat(v1.100 PR-26-code-D): post-restore evidence record (§39.3 / §48.6 lock) PR-26-code-D — restore verification / evidence hardening, slice D. Adds the structured post-restore evidence-record writer per §39.3 + §48.6 operator lock. Recording-only — does NOT re-run PR-24 decisions, rebuild TargetAuthority, or add validator/module-health probes (operator design call). Authority: - PR #512 / contract.md Part IV §§37-50 - PR #513 / §51 lock record - PR #514 / code-A merge 4e98ff5 - PR #515 / code-B merge 45fc63e - PR #516 / code-C merge 6d8386d - §39 Q1 BLOCKING evidence rows - §39.3 evidence-record file requirement - §46 CI gate requirements - §48.6 (operator-locked at this commit's open): - path: /var/lib/nftban/state/restore-evidence/ - filename: restore-evidence-<UTC-RFC3339-basic>-<short-random>.json - schema: 1.0.0 - writer helper: writeRestoreEvidenceRecord(ctx, exec, record) - path constant: restoreEvidenceDir - §51.5-A2 (read-only typed introspection outside mutation cap) Files added (2): cmd/nftban-installer/restore_evidence.go - Constants: restoreEvidenceSchemaVersion = "1.0.0" restoreEvidenceDir = "/var/lib/nftban/state/restore-evidence" restoreEvidenceFilenamePrefix = "restore-evidence-" restoreEvidenceMode = 0o640 restoreEvidenceDirMode = 0o750 - Schema types: RestoreEvidenceRecord (schema_version, timestamp_utc, mode, phase, target, result, verification, history_gate, warnings) + the 4 nested structs. - Sentinels: ErrEvidenceWriteFailed, ErrEvidenceNilExecutor, ErrEvidenceNilRecord. - writeRestoreEvidenceRecord — the SINGLE helper. MkdirAll, marshal, WriteFileAtomic. Filename: prefix + UTC RFC3339-basic stamp + "-" + 8-hex random suffix + ".json". - buildRestoreEvidenceRecord — recording-only assembler. Sources: target.Kind/FirewallType/Panel, execRes.Terminal/Stage/VerifyResult, exec.NftTableExists for emergency + nftban tables, detect.SSHPortWithSource. No re-derivation; no Probe / Decide / DetectPanel calls. - evidenceShortRandom — crypto/rand-backed 8-hex suffix to avoid same-second filename collisions. cmd/nftban-installer/restore_evidence_test.go - 10 tests: 1. WriteRestoreEvidence_HappyPath — filename pattern + single write 2. WriteRestoreEvidence_RoundTripsJSON — schema_version + mode + phase + history_gate flags 3. WriteRestoreEvidence_NilExecutor — defensive guard 4. WriteRestoreEvidence_NilRecord — defensive guard 5. WriteRestoreEvidence_OnlyHelperWritesUnderEvidenceDir_FileScan — single-WriteFileAtomic invariant 6. WriteRestoreEvidence_NoForbiddenSurfaces_FileScan — recording-only invariant pin 7. BuildRestoreEvidenceRecord_RecordedPriorHappy — full happy path with ss-listener SSH port resolution 8. BuildRestoreEvidenceRecord_NftbanTablesPresent_Recorded — post-mutation kernel observation 9. BuildRestoreEvidenceRecord_AuthorityClassDivergenceWarning — ObservedAuthority diverging from AuthorityExternal surfaces in warnings 10. RestoreEvidenceConstants_LockPin — §48.6 path/version/prefix pinned exactly Files modified (4): internal/installer/detect/ssh.go - Added detect.SSHPortWithSource (read-only). Same 4-source priority chain as detect.SSHPort but also returns the source name (ss / sshd_config / state / config) — required by the §48.6 schema's ssh_port_source enum. Per §51.5-A2 outside the mutation cap. cmd/nftban-installer/restore_decide.go - runRestoreExecutionFromProceed gains a Step D (between Execute and Transition): 1. buildRestoreEvidenceRecord(target, execRes) 2. writeRestoreEvidenceRecord(ctx, exec, rec, log) - §48.6 downgrade rule: if evidence-write fails AFTER a successful StateRestoreExecuted, downgrade to StateRestoreDegraded (state.machine.go:152 already supports this terminal). The state model supports the downgrade; no contract amendment needed. - Operator-facing log line on Degraded now includes the evidence- write failure reason. - No state-machine / exit-code / history-gate change. main.go:132 mode-gate untouched. cmd/nftban-installer/restore_decide_test.go - TestRunRestoreExecutionFromProceed_FakeDeps_HappyPath_PersistsExecuted + 4 other dispatcher tests updated: pass executor.NewMockExecutor() instead of nil so the new evidence-write step succeeds and the terminal stays at StateRestoreExecuted (fake happy path). The 3 tests that pass nil exec via _ = runRestoreExecutionFromProceed do not assert on sf.State so they still pass under the downgrade. .github/workflows/ci-restore-canonization.yml - New gate G4-RESTORE-EVIDENCE-RECORD (§46). Structural — pins the named-constant + single-helper invariant: * restore_evidence.go declares restoreEvidenceDir, restoreEvidenceSchemaVersion, restoreEvidenceFilenamePrefix verbatim + locked values * restore_evidence.go declares writeRestoreEvidenceRecord + buildRestoreEvidenceRecord + RestoreEvidenceRecord struct * exactly ONE WriteFileAtomic call in restore_evidence.go (the single-helper invariant — locked by §48.6) * forbidden-symbol scan: restore.Decide / restore.PlanFromDecision / uninstall.Probe / detect.DetectPanel / writeHistory / update-history.json / mutation primitives / direct OS bypass (recording-only invariant) * dispatcher (restore_decide.go) calls BOTH writeRestoreEvidenceRecord AND buildRestoreEvidenceRecord (proves evidence is consumed, not just imported) - §46.1 line-skipping discipline applied (production-code-only, comment-stripped). Recording-only invariant (operator design call) honored: - No restore.Decide / restore.PlanFromDecision calls - No uninstall.Probe call - No detect.DetectPanel call (only detect.SSHPortWithSource — read-only typed introspection) - No validator full-sweep / module-health probe - No update-history.json write (§19.2 layer 4 / main.go:132 retained) - No new mutation primitive Constraints honored (per operator scope): IN: - evidence record type + schema ✓ (§48.6 lock) - evidence writer helper ✓ (single helper writeRestoreEvidenceRecord) - production write after restore execution path ✓ (dispatcher Step D) - structural CI gate G4-RESTORE-EVIDENCE-RECORD ✓ - tests proving all writes stay under restoreEvidenceDir ✓ - tests proving update-history is untouched ✓ (HistoryGate flags + no writeHistory references in evidence module) OUT: - destructive soak (PR-26-code-E) - A.4 cron changes (already shipped in code-C) - executor new mutation methods (Stat is read-only, shipped in code-C) - iptables introspection (Option B lock) - main.go history gate changes (untouched) - state/exit-code changes — only the existing StateRestoreDegraded is consumed, no new state added - repo hygiene / UX / GOTH / metrics / module cleanup Verified on lab2 (Ubuntu 24.04, go1.22.2): - go build ./... clean - go test ./... PASS (full repo, 64 packages) - go test -race -count=1 cmd + restore + state + switchop + detect PASS - go vet ./... clean - go mod tidy no-op - 10 new TestWriteRestoreEvidence_* / TestBuildRestoreEvidenceRecord_* / TestRestoreEvidenceConstants_LockPin tests all PASS - existing 5 dispatcher fake-deps tests updated + still PASS - All 3 G4 gates (NO-OUT-OF-TARGET / CRON-MANIFEST-INTEGRITY / EVIDENCE-RECORD) local replay: FAIL=0 Awaiting auditor pass before push. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(v1.100 PR-26-code-D): add 5 dispatcher-level evidence-failure semantics tests (auditor checkpoint) Auditor focused-audit on 849b372 flagged that PR-26-code-D's Step D introduces a real operator-visible terminal transition: StateRestoreExecuted + evidence write failure → StateRestoreDegraded The 10 unit tests already covered the writer + builder + recording invariants but did NOT pin the dispatcher-level downgrade semantics. This commit adds 5 dispatcher-level tests to close that gap. Tests added: cmd/nftban-installer/restore_decide_test.go 1. PR26D_ExecutedPlusEvidenceFail_DowngradesToDegraded fake deps return StateRestoreExecuted; writeFailExec wrapper forces evidence WriteFileAtomic to fail. Asserts: - sf.State == StateRestoreDegraded (downgrade fires) - exit code == StateRestoreDegraded.ExitCode() - sf.State != StateRestoreExecuted (no false claim) Note: sf.FailureReason stays empty by design (Transition only populates FailureReason on .IsFailed() states; Degraded is success-with-warnings). The downgrade reason surfaces via log.Result, which is the authoritative operator channel for Degraded outcomes. 2. PR26D_FailedExecutionPlusEvidenceFail_TerminalPreserved fake.mutateErr forces FailedExecution; writeFailExec forces evidence-write failure. Asserts: - sf.State == StateRestoreFailedExecution (terminal preserved) - exit == StateRestoreFailedExecution.ExitCode() Evidence failure is warning-only on non-Executed terminals. 3. PR26D_FailedVerificationPlusEvidenceFail_TerminalPreserved fake.activeRet=false forces inline-verify SafeToRemove=false → FailedVerification; writeFailExec forces evidence-write fail. Asserts terminal + exit code unchanged from FailedVerification. 4. PR26D_ExecutedPlusEvidenceOk_PreservesExecuted Plain MockExecutor (writes succeed). Asserts: - sf.State == StateRestoreExecuted (no downgrade on clean write) - exit == StateRestoreExecuted.ExitCode() - exactly one file written under restoreEvidenceDir - no writes outside restoreEvidenceDir 5. PR26D_NoUpdateHistoryWrite_FileScan File-scan against restore_decide.go. Strips line-leading // per §46.1; asserts no production-code reference to writeHistory( or update-history.json. Pins the §19.2 layer-4 invariant stays untouched after PR-26-code-D adds Step D. writeFailExec wrapper (test-only): Wraps *executor.MockExecutor and overrides only WriteFileAtomic to fail. Avoids changing the production MockExecutor; uses the same composition pattern as flakyCSFActiveExec (introduced in PR-25 4B-3-csf for analogous test purposes). Verified on lab2 (Ubuntu 24.04, go1.22.2): - go build ./... clean - go test ./cmd/nftban-installer/... PASS - 5 new TestRunRestoreExecutionFromProceed_PR26D_* / TestDispatcher_PR26D_* tests all PASS - go test -race -count=1 cmd + restore + state PASS - existing PR-25 + PR-26-code-A/B/C tests still PASS No production code change. No CI workflow change. No contract amendment needed. Restore semantics from §48.6 lock + §19.2 layer-4 invariant are both now structurally pinned by tests. Awaiting auditor sign-off + push signal. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR-26-code-C: cron backup manifest + A.4 manifest restore
This PR is split internally into 4 reviewable commits on the same branch:
96162f69switchop.disarmCSFArtifacts+ new sharedcron_manifest.go(sha256-pinned manifest types,ComputeCronBackupSHA256,WriteCronBackupManifest,ReadCronBackupManifest,VerifyCronBackupEntry) + read-only typedexecutor.Stat(per §51.5-A2 outside the mutation cap).c5767f45restore_deps_csf.go::mutateToCSFTargetstep 3. Two new typed sentinels:ErrCSFRestoreCronManifestCorrupt,ErrCSFRestoreCronTargetExists.93e86e25G4-RESTORE-CRON-MANIFEST-INTEGRITY— pins writer + reader symbols, allow-list cron-target literals, §46.1 line-skipping discipline.f7be0c49Authority
contract.mdPart IV §§37–504e98ff5645fc63efEvidence model
ErrCSFRestoreCronManifestCorruptErrCSFRestoreCronManifestCorruptErrCSFRestoreCronManifestCorruptErrCSFRestoreCronManifestCorruptErrCSFRestoreCronManifestCorruptErrCSFRestoreCronTargetExistsRationale: absence = migration tolerance for pre-PR-26 hosts (graceful soft-skip with operator warning, A.5 continues). Corruption = untrusted evidence (HARD refuse before A.5 — the existing §32 step-3 failure path retains the safety net; operator must inspect). This alignment was applied per the focused-audit verdict on
f7be0c49.Manifest schema (locked at §42.2)
/var/lib/nftban/state/csf-cron-backup/csf-cron,lfd-cron(the sha256-pinned content copies)manifest.json—schema_version+captured_at+ per-entry{path, backup_name, sha256, mode, uid, gid, size}1.0.0(any drift is rejected withErrCronManifestSchemaMismatch)/etc/cron.d/csf-cronAND/etc/cron.d/lfd-cronONLY. Unknown-entry paths refused.Pre-PR-26 hosts without manifest
By design: graceful soft-skip with operator warning. A.5 continues so csf can still start. The migration is purely additive — pre-PR-26 hosts do not require manifest creation.
Out of scope (not touched)
IptablesRuleExists/ iptables introspection (Option B lock)main.go/ state machine / exit codes / history gate (INV-PR25-HISTORY-GATE retained)TargetAuthority/ PR-24 latticecontract.md(no amendment needed; §42 lock satisfied as-is)Files changed (8)
internal/installer/executor/executor.goFileMeta+Stat(read-only)internal/installer/executor/real.goRealExecutor.Statvia os.Stat + syscall.Stat_tinternal/installer/executor/mock.goMockExecutor.Stat+FileStatsmapinternal/installer/switchop/cron_manifest.gointernal/installer/switchop/cron_manifest_test.gointernal/installer/switchop/takeover.godisarmCSFArtifactswrites manifest before rmcmd/nftban-installer/restore_deps_csf.gocmd/nftban-installer/restore_deps_csf_test.goseedCronManifesthelper.github/workflows/ci-restore-canonization.ymlG4-RESTORE-CRON-MANIFEST-INTEGRITYstructural gateLab2 verification (head
f7be0c49)go build ./...cleango test ./...PASS (full repo, 64 packages)go test -race -count=1cmd + restore + state + switchop PASSgo vet ./...cleango mod tidyno-opG4-RESTORE-CRON-MANIFEST-INTEGRITYlocal gate replay:FAIL=0Test plan
Restore Canonization Gatematrix (ubuntu-24.04 + almalinux-9 + summary) greenG4-RESTORE-EXEC-NO-OUT-OF-TARGETgreen (no new mutation symbols outside the bounded-3 cap)G4-RESTORE-CRON-MANIFEST-INTEGRITY(NEW) greenG4-RESTORE-NO-IMPLICIT-EXECgreenArchitecture Policy / Policy GatesgreenGo Build & Test+ race + full DEB+RPM matrix + CodeQL / Semgrep / Secure Go / OSV / Gitleaks / GitGuardian greenShellCheck/Bash Validation/Docs QualitygreenAfter CI completes, audit the gate results before merge.
🤖 Generated with Claude Code